#include "xmppcore/connection.h"
#include "xmppcore/connector.h"
#include "xmppcore/initialstreamnegotiant.h"
#include "xmppcore/tlsstreamfeature.h"
#include "xmppcore/tlsnegotiant.h"
#include "xmppcore/constants.h"
#include "xmppcore/predefinedxmppparsingfactory.h"

namespace yoohoo_xmppcore {

Connection::Connection(ConnectionConfig& config, QObject* parent)  : QObject(parent) {
	_server = new Server(config.server());
	_isClientTlsRequired = config.requireTls();
	_resource = config.resource();
	_xmppState = XmppStateUnconnected;
	_connector = 0;
	_initialStreamNegotiant = 0;
	_tlsNegotiant = 0;
	_saslNegotiant = 0;
	_resourceBindingNegotiant = 0;
	_sessionNegotiant = 0;
	_tlsRequired = false;
	_closeStreamSignalReceived = false;
	_parsingFactory = new PredefinedXmppParsingFactory;
}

Connection::~Connection() {
	qDebug("Connection::~Connection(): Release connection");

	disconnectFromHost();
	
	if (_server) {
		delete _server;
	}

	if (_parsingFactory) {
		delete _parsingFactory;
	}
}

void Connection::connectToHost() {
	_closeStreamSignalReceived = false;
	_connectionError = NoError;
	_connectionState = UnconnectedState;
	
	if (_connector) {
		delete _connector;
		_connector = 0;
	}

	createConnector();
}

void Connection::disconnectFromHost() {
	destroyNegotiants();

	if (_connector) {
		delete _connector;
		_connector = 0;
	}

	foreach (StreamFeatureFactory* factory, _streamFeatureFactories) {
		if (factory) {
			delete factory;
		}
	}
	_streamFeatureFactories.clear();

	_supportedStreamFeatures.clear();
	_closeStreamSignalReceived = false;
	_xmppState = XmppStateUnconnected;
	_tlsRequired = false;
}

void Connection::changeThread(QThread* thread) {
	moveToThread(thread);
}

void Connection::createConnector() {
	if (!_connector) {
		_connector = new Connector(*_server);
		if (_tlsCallback != 0) {
			_connector->setTlsCallback(_tlsCallback);
		}

		connect(_connector, SIGNAL(socketError(QAbstractSocket::SocketError)), this, SLOT(processSocketError(QAbstractSocket::SocketError)));
		connect(_connector, SIGNAL(socketStateChanged(QAbstractSocket::SocketState)), this, SLOT(processSocketStateChanged(QAbstractSocket::SocketState)));
		connect(_connector, SIGNAL(socketReady()), this, SLOT(processSocketReady()));
		connect(_connector, SIGNAL(received(QString)), this, SLOT(processReceived(QString)));
		connect(_connector, SIGNAL(write(QString)), this, SLOT(processWrite(QString)));
	}

	if (_connector) {
		_connector->connectToHost();
	}
}

void Connection::processSocketError(QAbstractSocket::SocketError error) {
	switch (error) {
	case QAbstractSocket::ConnectionRefusedError:
		_connectionError = Connection::ConnectionRefusedError;
		break;
	case QAbstractSocket::RemoteHostClosedError:
		_connectionError = Connection::RemoteHostClosedError;
		break;
	case QAbstractSocket::HostNotFoundError:
		_connectionError = Connection::HostNotFoundError;
		break;
	case QAbstractSocket::SocketAccessError:
		_connectionError = Connection::SocketAccessError;
		break;
	case QAbstractSocket::SocketResourceError:
		_connectionError = Connection::SocketResourceError;
		break;
	case QAbstractSocket::SocketTimeoutError:
		_connectionError = Connection::SocketTimeoutError;
		break;
	case QAbstractSocket::DatagramTooLargeError:
		_connectionError = Connection::DatagramTooLargeError;
		break;
	case QAbstractSocket::NetworkError:
		_connectionError = Connection::NetworkError;
		break;
	case QAbstractSocket::AddressInUseError:
		_connectionError = Connection::AddressInUseError;
		break;
	case QAbstractSocket::SocketAddressNotAvailableError:
		_connectionError = Connection::SocketAddressNotAvailableError;
		break;
	case QAbstractSocket::UnsupportedSocketOperationError:
		_connectionError = Connection::UnsupportedSocketOperationError;
		break;
	case QAbstractSocket::UnfinishedSocketOperationError:
		_connectionError = Connection::UnfinishedSocketOperationError;
		break;
	case QAbstractSocket::ProxyAuthenticationRequiredError:
		_connectionError = Connection::ProxyAuthenticationRequiredError;
		break;
	case QAbstractSocket::SslHandshakeFailedError:
		_connectionError = Connection::SslHandshakeFailedError;
		break;
	case QAbstractSocket::ProxyConnectionRefusedError:
		_connectionError = Connection::ProxyConnectionRefusedError;
		break;
	case QAbstractSocket::ProxyConnectionClosedError:
		_connectionError = Connection::ProxyConnectionClosedError;
		break;
	case QAbstractSocket::ProxyConnectionTimeoutError:
		_connectionError = Connection::ProxyConnectionTimeoutError;
		break;
	case QAbstractSocket::ProxyNotFoundError:
		_connectionError = Connection::ProxyNotFoundError;
		break;
	case QAbstractSocket::ProxyProtocolError:
		_connectionError = Connection::ProxyProtocolError;
		break;
	case QAbstractSocket::UnknownSocketError:
	default:
		_connectionError = Connection::UnknownSocketError;
	}

	if (!_closeStreamSignalReceived) {
		emit connectionError(_connectionError);
	}
}

void Connection::processSocketStateChanged(QAbstractSocket::SocketState state) {
	switch (state) {
	case QAbstractSocket::UnconnectedState:
		_connectionState = Connection::UnconnectedState;
		break;
	case QAbstractSocket::HostLookupState:
		_connectionState = Connection::HostLookupState;
		break;
	case QAbstractSocket::ConnectingState:
		_connectionState = Connection::ConnectingState;
		break;
	case QAbstractSocket::ConnectedState:
		_connectionState = Connection::ConnectedState;
		break;
	case QAbstractSocket::ClosingState:
		_connectionState = Connection::ClosingState;
		break;
	case QAbstractSocket::BoundState:
	case QAbstractSocket::ListeningState:
	default:
		_connectionState = Connection::UnknownState;
	}

	emit connectionState(_connectionState);
}

void Connection::processSocketReady() {
	_xmppState = XmppStateStreamNegotiating;
	if (!_initialStreamNegotiant) {
		_initialStreamNegotiant = new InitialStreamNegotiant(_connector);
		registerStreamFeatureFromFactory(_initialStreamNegotiant, StreamFeatureFactory::INITIAL);
		connect(_initialStreamNegotiant, SIGNAL(written(QString)), this, SLOT(processNegotiationWritten(QString)));
		connect(_initialStreamNegotiant, SIGNAL(error(Negotiant::NegotiationError)), this, SLOT(processNegotiationError(Negotiant::NegotiationError)));
		connect(_initialStreamNegotiant, SIGNAL(done(StreamFeatures*)), this, SLOT(processInitialStreamDone(StreamFeatures*)));
	}
	_initialStreamNegotiant->negotiate();
}

void Connection::registerStreamFeatureFromFactory(Negotiant* negotiant,
		StreamFeatureFactory::FeatureAdvertisementPhase phase) {
	foreach (StreamFeatureFactory* factory, _streamFeatureFactories) {
		if ((factory->phases() & phase) == phase) {
			negotiant->registerStreamFeature(factory->createStreamFeature());
		}
	}
}

void Connection::processInitialStreamDone(StreamFeatures* streamFeatures) {
	TlsStreamFeature* tlsFeature = (TlsStreamFeature*)streamFeatures->getStreamFeature(
		XMPP_START_TLS_NAME, XMPP_TLS_NAMESPACE);
	if (_isClientTlsRequired || (tlsFeature != 0 && tlsFeature->isRequired()))
		_tlsRequired = true;

	if (_tlsRequired) {
		destroyNegotiants();
		if (!_tlsNegotiant) {
			_tlsNegotiant = new TlsNegotiant(_connector);
			registerStreamFeatureFromFactory(_tlsNegotiant, StreamFeatureFactory::TLS);
			connect(_tlsNegotiant, SIGNAL(written(QString)), this, SLOT(processNegotiationWritten(QString)));
			connect(_tlsNegotiant, SIGNAL(error(Negotiant::NegotiationError)), this, SLOT(processNegotiationError(Negotiant::NegotiationError)));
			connect(_tlsNegotiant, SIGNAL(done(StreamFeatures*)), this, SLOT(processTlsDone(StreamFeatures*)));
		}

		_tlsNegotiant->negotiate();
	} else {
		saveSupportedStreamFeatures(_initialStreamNegotiant);
		destroyNegotiants();
		_xmppState = XmppStateWaitForAuthenticating;
		emit readyToAuthenticate();
	}
}

void Connection::processTlsDone(StreamFeatures* streamFeatures) {
	saveSupportedStreamFeatures(_tlsNegotiant);
	destroyNegotiants();
	_xmppState = XmppStateWaitForAuthenticating;
	emit readyToAuthenticate();
}

void Connection::destroyNegotiants() {
	if (_initialStreamNegotiant) {
		delete _initialStreamNegotiant;
		_initialStreamNegotiant = 0;
	}

	if (_tlsNegotiant) {
		delete _tlsNegotiant;
		_tlsNegotiant = 0;
	}

	if (_saslNegotiant) {
		delete _saslNegotiant;
		_saslNegotiant = 0;
	}

	if (_resourceBindingNegotiant) {
		delete _resourceBindingNegotiant;
		_resourceBindingNegotiant = 0;
	}

	if (_sessionNegotiant) {
		delete _sessionNegotiant;
		_sessionNegotiant = 0;
	}
}

void Connection::saveSupportedStreamFeatures(Negotiant* negotiant) {
	_supportedStreamFeatures.clear();
	QList<StreamFeature*> streamFeatures = negotiant->streamFeatures()->streamFeatures();
	foreach (StreamFeature* feature, streamFeatures) {
		StreamFeaturePair featurePair;
		featurePair.name = feature->name();
		featurePair.namespaceUri = feature->namespaceUri();
		_supportedStreamFeatures.append(featurePair);
	}
}

void Connection::processWritten(QString xml) {
	emit written(xml);
}

void Connection::processNegotiationError(Negotiant::NegotiationError error) {
	destroyNegotiants();
	switch (error) {
	case Negotiant::UnknownNegotiationProtocolError:
		_connectionError = Connection::UnknownNegotiationProtocolError;
		break;
	default:
		_connectionError = Connection::UnknownNegotiationError;
	}

	if (!_closeStreamSignalReceived) {
		emit connectionError(_connectionError);
	}
}

void Connection::setTlsCallback(TlsCallback* tlsCallback) {
	_tlsCallback = tlsCallback;
}
bool Connection::isClientTlsRequired() {
	return _isClientTlsRequired;
}

QString Connection::bindedJid() {
	return _bindedJid;
}

Connection::XmppState Connection::xmppState() {
	return _xmppState;
}

Connection::ConnectionState Connection::connectionState() {
	return _connectionState;
}

Connection::ConnectionError Connection::connectionError() {
	return _connectionError;
}

void Connection::processReceived(QString xml) {
	emit received(xml);

	if (xml == "</stream:stream>") {
		_closeStreamSignalReceived = true;
		emit streamClosed();

		return;
	}

	if (_xmppState == XmppStateUnconnected || _xmppState == XmppStateStreamNegotiating) {
		return;
	}

	XmppElement* element = _parsingFactory->parse(xml);
	if (!element) {
		emit connectionError(UnknownProtoclError);
		return;
	}

	StreamError* _streamError = dynamic_cast<StreamError*>(element);
	if (_streamError) {
		emit streamError(*_streamError);
		delete _streamError;

		return;
	}
	
	StanzaError* _stanzaError = dynamic_cast<StanzaError*>(element);
	if (_stanzaError) {
		emit stanzaError(*_stanzaError);
		delete _stanzaError;

		return;
	}

	if (_xmppState == XmppStateWaitForAuthenticating && xml.startsWith("<stream:")) {
		return;
	}

	Iq* iq = dynamic_cast<Iq*>(element);
	if (_xmppState == XmppStateWaitForAuthenticating && !iq) { // can only process iq stanza while waiting for authenticating
		emit connectionError(UnknownProtoclError);

		delete element;
		return;
	}

	if (_xmppState == XmppStateSessionEstablished) {
		if (iq) {
			emit iqReceived(*iq);
		} else {
			Message* message = dynamic_cast<Message*>(element);
			if (message) {
				emit messageReceived(*message);
			} else {
				Presence* presence = dynamic_cast<Presence*>(element);

				if (presence) {
					emit presenceReceived(*presence);
				}
			}
		}
	}

	delete element;
}

void Connection::authenticate(QString userName, QString password) {
	if (_connectionState != Connection::ConnectedState) {
		qDebug("Connection::authenticate(): authenticating at unconnected state.");

		return;
	}

        if (!_saslNegotiant) {
                _saslNegotiant = new SaslNegotiant(_connector, userName, password, _server->host());
		registerStreamFeatureFromFactory(_saslNegotiant, StreamFeatureFactory::SASL);
		connect(_saslNegotiant, SIGNAL(written(QString)), this, SLOT(processNegotiantWritten(QString)));
		connect(_saslNegotiant, SIGNAL(error(Negotiant::NegotiationError)), this, SLOT(processNegotiantError(Negotiant::NegotiationError)));
		connect(_saslNegotiant, SIGNAL(saslError(SaslNegotiant::SaslError)), this, SLOT(processSaslError(SaslNegotiant::SaslError)));
		connect(_saslNegotiant, SIGNAL(done(StreamFeatures*)), this, SLOT(processSaslDone(StreamFeatures*)));
		connect(_saslNegotiant, SIGNAL(success()), this, SLOT(processSaslSuccess()));
	}
}

void Connection::processSaslDone(StreamFeatures* streamFeatures) {
	destroyNegotiants();
	if (!_resourceBindingNegotiant) {
		_resourceBindingNegotiant = new ResourceBindingNegotiant(_connector, _resource);
		connect(_resourceBindingNegotiant, SIGNAL(written(QString)), this, SLOT(processNegotiantWritten(QString)));
		connect(_resourceBindingNegotiant, SIGNAL(error(Negotiant::NegotiationError)), this, SLOT(processNegotiantError(Negotiant::NegotiationError)));
		connect(_resourceBindingNegotiant, SIGNAL(success(QString)), this, SLOT(processResourceBindingSuccess(QString)));
		connect(_resourceBindingNegotiant, SIGNAL(resourceBindingError(ResourceBindingError)), this, SLOT(processResourceBindingError(ResourceBindingError)));
	}
	_resourceBindingNegotiant->negotiate();
}

void Connection::processSaslSuccess() {
	qDebug("Connection::processSaslSuccess(): authenticated");
	_xmppState = XmppStateAuthenticated;
	emit authenticated();
}

void Connection::processSaslError(SaslNegotiant::SaslError error) {
	destroyNegotiants();
	if (error == SaslNegotiant::SaslErrorNotSupported) {
		emit connectionError(SaslNotSupportedError);
	} else if (error == SaslNegotiant::SaslErrorInit) {
		emit connectionError(SaslInitError);
	} else if (error == SaslNegotiant::SaslErrorHandshake) {
		emit connectionError(SaslHandshakeError);
	} else if (error == SaslNegotiant::SaslErrorCrypt) {
		emit connectionError(SaslCryptError);
	} else if (error == SaslNegotiant::SaslErrorAborted) {
		emit authFailure(AuthFailureAborted);
	} else if (error == SaslNegotiant::SaslErrorIncorrectEncoding) {
		emit authFailure(AuthFailureIncorrectEncoding);
	} else if (error == SaslNegotiant::SaslErrorInvalidAuthzid) {
		emit authFailure(AuthFailureInvalidAuthzid);
	} else if (error == SaslNegotiant::SaslErrorInvalidMechanism) {
		emit authFailure(AuthFailureInvalidMechanism);
	} else if (error == SaslNegotiant::SaslErrorMechanismTooWeak) {
		emit authFailure(AuthFailureMechanismTooWeak);
	} else if (error == SaslNegotiant::SaslErrorNotAuthorized) {
		emit authFailure(AuthFailureNotAuthorized);
	} else if (error == SaslNegotiant::SaslErrorTemporaryAuthFailure) {
		emit authFailure(AuthFailureTemporaryAuthFailure);
	} else if (error == SaslNegotiant::SaslErrorUnknown) {
		emit connectionError(UnknownSaslError);
	} else {
		emit connectionError(UnknownSaslError);
	}
}

void Connection::processResourceBindingError(ResourceBindingNegotiant::ResourceBindingError error) {
	destroyNegotiants();
	if (error == ResourceBindingNegotiant::ConflictError) {
		emit resourceBindingError(BindingErrorConflict);
	} else if (ResourceBindingNegotiant::ConflictError) {
		emit resourceBindingError(BindingErrorNotAllowed);
	} else {
		emit resourceBindingError(BindingErrorForbidden);
	}
}

void Connection::processResourceBindingSuccess(QString jid) {
	qDebug("Connection::processResourceBindingSuccess(): resource binded.");

	destroyNegotiants();
	_xmppState = XmppStateResourceBinded;
	_bindedJid = jid;
	emit resourceBinded(jid);
	
	if (!_sessionNegotiant) {
		_sessionNegotiant = new SessionNegotiant(_connector);
		connect(_sessionNegotiant, SIGNAL(written(QString)), this, SLOT(processNegotiantWritten(QString)));
		connect(_sessionNegotiant, SIGNAL(error(Negotiant::NegotiationError)), this, SLOT(processNegotiantError(Negotiant::NegotiationError)));
		connect(_sessionNegotiant, SIGNAL(success()), this, SLOT(processSessionEstablishementSuccess()));
		connect(_sessionNegotiant, SIGNAL(sessionEstablishmentError(SessionEstablishmentError)), this, SLOT(processSessionEstablishmentError(SessionEstablishmentError)));
	}
	_sessionNegotiant->negotiate();
}

void Connection::processSessionEstablishementSuccess() {
	qDebug("Connection::processSessionEstablishementSuccess(): session establishment.");
	
	destroyNegotiants();
	_xmppState = XmppStateSessionEstablished;
	emit sessionEstablished();
}

void Connection::processSessionEstablishmentError(SessionNegotiant::SessionEstablishmentError error) {
	destroyNegotiants();
	if (error == SessionNegotiant::ConflictError) {
		emit sessionEstablishmentError(SessionEstablishmentErrorConflict);
	} else if (error == SessionNegotiant::InternalServerError) {
		emit sessionEstablishmentError(SessionEstablishmentErrorInternalServer);
	} else {
		emit sessionEstablishmentError(SessionEstablishmentErrorForbidden);
	}
}

void Connection::registerStreamFeature(StreamFeatureFactory* streamFeatureFactory) {
	_streamFeatureFactories.append(streamFeatureFactory);
}

bool Connection::isStreamFeatureSupported(QString name, QString namespaceUri) {
	if (_xmppState == XmppStateUnconnected || _xmppState == XmppStateStreamNegotiating)
		return false;

	foreach (StreamFeaturePair featurePair, _supportedStreamFeatures) {
		if (featurePair.name == name && featurePair.namespaceUri == namespaceUri) {
			return true;
		}
	}

	return false;
}

void Connection::sendMessage(Message message, Connection* connection) {
	QMetaObject::invokeMethod(connection, "sendMessage", Q_ARG(Message, message));
}

void Connection::sendPresence(Presence presence, Connection* connection) {
	QMetaObject::invokeMethod(connection, "sendPresence", Q_ARG(Presence, presence));
}

void Connection::sendIq(Iq iq, Connection* connection) {
	QMetaObject::invokeMethod(connection, "sendIq", Q_ARG(Iq	, iq));
}

void Connection::write(QString xml, Connection* connection) {
	QMetaObject::invokeMethod(connection, "write", Q_ARG(QString, xml));
}

void Connection::sendMessage(Message message) {
	_connector->write(message.toXml());
}

void Connection::sendPresence(Presence presence) {
	_connector->write(presence.toXml());
}

void Connection::sendIq(Iq iq) {
	if (_xmppState != XmppStateWaitForAuthenticating &&
		_xmppState != XmppStateAuthenticated &&
		_xmppState != XmppStateSessionEstablished) {
			qDebug("Connection::sendIq(): Illegal state[%d], abandon a iq stanza", _xmppState);
			return;
	}

	_connector->write(iq.toXml());
}

void Connection::write(QString xml) {
	_connector->write(xml);
}

}
